﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Common;
using Gemli.Collections;

namespace Gemli.Data
{
    /// <summary>
    /// Represents a database table bound entity.
    /// </summary>
    [Serializable]
    public partial class DataModel : INotifyPropertyChanged, INotifyPropertyChanging
    {
        /// <summary>
        /// Raised when one of the inner data dictionary values has been modified,
        /// or else when the object's dirty state has been reset.
        /// </summary>
        public event EventHandler DirtyStateChanged;

        /// <summary>
        /// Raised when the inner data dictionary has changed.
        /// </summary>
        protected event PropertyChangedEventHandler DataChanged;
        /// <summary>
        /// Raised just before the inner data dictionary changes.
        /// </summary>
        protected event PropertyChangingEventHandler DataChanging;

        /// <summary>
        /// Raised just before the data gets loaded from the <see cref="DataProvider"/>.
        /// </summary>
        protected event EventHandler DataLoading;
        /// <summary>
        /// Raised when the data has been loaded from the <see cref="DataProvider"/>.
        /// </summary>
        protected event EventHandler DataLoaded;

        /// <summary>
        /// Raised after an instance of a <see cref="DataModel"/>
        /// has been created and initialized.
        /// </summary>
        public static event EventHandler Created;

        ///// <summary>
        ///// Raised after a Collection instance of <see cref="DataModel"/>
        ///// has been created and initialized.
        ///// </summary>
        //public static event EventHandler CollectionCreated;

        private bool _IsDirty;
        private bool _IsNew = true;
        private bool DataChangeDirtyHandlerInit;

        /// <summary>
        /// Constructs and initializes an empty DataModel object.
        /// </summary>
        public DataModel()
        {
            Initialize(this);
        }

        /// <summary>
        /// Constructs and initializes a typed DataModel object.
        /// </summary>
        /// <param name="type"></param>
        protected DataModel(Type type)
        {
            Initialize(this.Entity, type);
        }

        /// <summary>
        /// Constructs and initializes a typed DataModel object
        /// and populates the inner data dictionary with the members
        /// of the specified <paramref name="modelInstance"/>.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="modelInstance"></param>
        protected DataModel(Type type, object modelInstance)
        {
            Initialize(modelInstance, type);
        }

        /// <summary>
        /// Loads the metadata (mappings, types, etc) for this DataModel.
        /// </summary>
        /// <param name="type"></param>
        private void PreInitialize(Type type)
        {
            InnerData = new CaseInsensitiveDictionary<object>();
            EntityMappings = GetMapping(type);
            ColumnMappedValue = new ColumnMappedValueProperty(this, EntityMappings);
            Convert = new DataModelConverter(this);

            DataChanging += DataModel_DataChanging;
            DataChanged += DataModel_DataChanged;
            ModifiedProperties = new CaseInsensitiveStringList();
        }

        /// <summary>
        /// Returns a list of database-bound CLR properties/fields 
        /// that have been modified since the last time this object
        /// was loaded or saved.
        /// </summary>
        public CaseInsensitiveStringList ModifiedProperties { get; set; }

        void DataModel_DataChanged(object sender, PropertyChangedEventArgs e)
        {
            if (!ModifiedProperties.Contains(e.PropertyName))
            ModifiedProperties.Add(e.PropertyName);
        }

        void DataModel_DataChanging(object sender, PropertyChangingEventArgs e)
        {
            if (!OriginalData.ContainsKey(e.PropertyName))
                OriginalData[e.PropertyName] = this[e.PropertyName];
        }

        /// <summary>
        /// Returns the database mapping metadata for type 
        /// <typeparamref name="T">T</typeparamref>.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static DataModelMap GetMapping<T>()
        {
            return GetMapping(typeof(T));
        }

        /// <summary>
        /// Returns the unwrapped type for the specified DataModel type. 
        /// For example, if DataModel&lt;Customer&gt; 
        /// is supplied in the parameter, returns Customer.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        public static Type GetUnwrappedType(Type t)
        {
            if (!t.IsDataModel()) return t;
            if (t.IsGenericType)
            {
                var typeArgs = t.GetGenericArguments();
                if (typeArgs.Length == 1)
                {
                    var g = typeof (DataModel<>).MakeGenericType(typeArgs);
                    if (g == t)
                    {
                        t = typeArgs[0];
                    }
                }
            }
            return t;
        }

        private static DataModelMap GetMapping(Type t)
        {
            return DataModelMap.GetEntityMapping(t);
        }

        private void DataModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            IsDirty = true;
        }

        /// <summary>
        /// Initializes the DataModel with the specified <paramref name="modelInstance"/>
        /// as its initial data load.
        /// </summary>
        /// <param name="modelInstance"></param>
        protected void Initialize(object modelInstance)
        {
            Initialize(modelInstance, modelInstance.GetType());
        }

        private bool _1stInit = true;
        /// <summary>
        /// Initializes the DataModel with the specified <paramref name="modelInstance"/>
        /// as its initial data load. The <paramref name="type"/> parameter should match
        /// modelInstance.GetType().
        /// </summary>
        /// <param name="modelInstance"></param>
        /// <param name="type"></param>
        private void Initialize(object modelInstance, Type type)
        {
            Entity = (modelInstance != null && 
                modelInstance is DataModel &&
                ((DataModel)modelInstance).Entity != null)
                ? ((DataModel)modelInstance).Entity
                : modelInstance;
            PreInitialize(type);

            Loading = true;
            foreach (var fieldmap in EntityMappings.FieldMappings)
            {
                if (!fieldmap.Value.DefaultValueDefined &&
                    fieldmap.Value.IsIdentity && !fieldmap.Value.IncludeOnInsert)
                {
                    InnerData[fieldmap.Key] = DBNull.Value;
                }
                else InnerData[fieldmap.Key] = NullToDbNull(fieldmap.Value.DefaultValue);
            }
            SynchronizeFields(SyncTo.FieldMappedData);
            if (OriginalData == null)
            {
                OriginalData = new CaseInsensitiveDictionary<object>(InnerData);
            }
            Loading = false;

            if (!DataChangeDirtyHandlerInit)
            {
                DataChanged += DataModel_PropertyChanged;
                DataChangeDirtyHandlerInit = true;
            }
            if (_1stInit && Created != null)
            {
                Created(this, new EventArgs());
            }
            _1stInit = false;
        }

        /// <summary>
        /// Performs a special reset on this DataModel object.
        /// </summary>
        /// <seealso cref="ResetMode"/>
        /// <param name="resetMode"></param>
        protected void Reset(ResetMode resetMode)
        {
            if (resetMode != ResetMode.RetainNotDirty)
            {
                if (resetMode == ResetMode.RevertNotDirty)
                {
                    InnerData = OriginalData;
                    SynchronizeFields(SyncTo.ClrMembers);
                }
                else Initialize(Entity);
            }
            _IsDirty = false;
            MarkDeleted = false;
            _IsNew = resetMode == ResetMode.ClearAndNew;
            OriginalData = new CaseInsensitiveDictionary<object>(InnerData);
        }

        /// <summary>
        /// Gets the unwrapped CLR object that this DataModel object
        /// represents to the database.
        /// </summary>
        public object Entity
        {
            get { return _Entity ?? this; }
            protected set { _Entity = value; }
        }
        private object _Entity { get; set; }

        /// <summary>
        /// Gets or sets the value identified by the specified 
        /// <paramref name="key"/>, which corresponds to the 
        /// CLR property/field. To perform the same indexing
        /// on the mapped database column name instead, use 
        /// the <see cref="ColumnMappedValue"/> property.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        protected internal object this[string key]
        {
            get
            {
                if (InnerData.ContainsKey(key))
                {
                    return DbNullToNull(InnerData[key]);
                }
                if (key.ToLower().StartsWith("field:") && key.Length > 6)
                {
                    var prop = key.Substring(6);
                    foreach (var fmap in EntityMappings.FieldMappings)
                    {
                        if (fmap.Value.ColumnName.ToLower() == prop.ToLower())
                        {
                            key = fmap.Key;
                            if (InnerData.ContainsKey(key))
                            {
                                return DbNullToNull(InnerData[key]);
                            }
                            break;
                        }
                    }
                }
                return InnerData[key] = DBNull.Value;
            }
            set
            {
                var fieldmapping = this.EntityMappings.FieldMappings[key];
                if ((value == null || value == DBNull.Value) &&
                    !fieldmapping.IsNullable &&
                    !(fieldmapping.IsIdentity && (this.IsNew || this.Loading)))
                {
                    throw new ArgumentException("Field \"" + key + "\" is mapped as IsNullable=False, "
                    + "but a null value was specified.");
                }
                if (DataChanging != null && !Loading)
                {
                    DataChanging(this, new PropertyChangingEventArgs(key));
                }
                InnerData[key] = NullToDbNull(value);
                if (DataChanged != null && !Loading)
                {
                    DataChanged(this, new PropertyChangedEventArgs(key));
                }
            }
        }

        private static object NullToDbNull(object obj)
        {
            if (null == obj) return DBNull.Value;
            return obj;
        }

        private static object DbNullToNull(object obj)
        {
            if (obj == DBNull.Value) return null;
            return obj;
        }

        /// <summary>
        /// Returns true if this entity was not loaded from a database record.
        /// </summary>
        public virtual bool IsNew
        {
            get { return _IsNew; }
            set { _IsNew = value; }
        }

        /// <summary>
        /// Returns true if this entity's inner data dictionary has been changed
        /// since it was last saved or loaded.
        /// </summary>
        public virtual bool IsDirty
        {
            get { return _IsDirty; }
            set
            {
                bool oldValue = _IsDirty;
                _IsDirty = value;
                if (value != oldValue)
                {
                    if (DirtyStateChanged != null && !Loading)
                        DirtyStateChanged(this, new EventArgs());
                }
            }
        }

        /// <summary>
        /// Gets or sets whether to delete this entity the next time it is saved.
        /// </summary>
        public bool MarkDeleted { get; set; }

        /// <summary>
        /// The inner data dictionary for the record.
        /// </summary>
        protected CaseInsensitiveDictionary<object> InnerData { get; private set; }

        /// <summary>
        /// Gets or sets the data that was loaded before changes were made.
        /// </summary>
        public CaseInsensitiveDictionary<object> OriginalData { get; set; }

        /// <summary>
        /// Returns the database mapping metadata associated with this entity.
        /// </summary>
        public DataModelMap EntityMappings { get; protected set; }

        /// <summary>
        /// Loads data from an <see cref="IDataReader"/>.
        /// </summary>
        /// <param name="dr"></param>
        public void Load(IDataReader dr)
        {
            BeginLoadingData();
            Convert.FromDataReader(dr);
            EndLoadingData();
        }

        /// <summary>
        /// Cache DataTable; use with collection
        /// </summary>
        /// <param name="dr"></param>
        /// <param name="dt"></param>
        internal void Load(IDataReader dr, DataTable dt)
        {
            BeginLoadingData();
            Convert.FromDataReader(dr, dt);
            EndLoadingData();
        }

        /// <summary>
        /// Loads data from a <see cref="DataRow"/>.
        /// </summary>
        /// <param name="dr"></param>
        public void Load(DataRow dr)
        {
            BeginLoadingData();
            Convert.FromDataRow(dr);
            EndLoadingData();
        }

        /// <summary>
        /// When overridden with <see cref="DataModel&lt;T&gt;"/>, prepares the 
        /// data to propagate to or from the <see cref="Entity"/> before 
        /// the Entity is retrieved or before data changes are made and/or saved.
        /// </summary>
        /// <param name="syncTo"></param>
        public virtual void SynchronizeFields(SyncTo syncTo) { }

        private bool Loading;
        private void BeginLoadingData()
        {
            Loading = true;
            if (DataLoading != null)
                DataLoading(this, new EventArgs());
        }

        private void EndLoadingData()
        {
            SynchronizeFields(SyncTo.ClrMembers);
            IsNew = false;
            IsDirty = false;
            Loading = false;
            if (DirtyStateChanged != null)
                DirtyStateChanged(this, new EventArgs());
            OriginalData = new CaseInsensitiveDictionary<object>(InnerData);
            if (DataLoaded != null)
                DataLoaded(this, new EventArgs());

        }

        /// <summary>
        /// Returns a utlity for the conversion and migration 
        /// of data between CLR and database on behalf of this entity.
        /// </summary>
        public DataModelConverter Convert { get; private set; }

        /// <summary>
        /// Pushes all CLR changes in this entity to the mapped database.
        /// </summary>
        public void Save()
        {
            Save(false, null);
        }

        /// <summary>
        /// Pushes all CLR changes in this entity to the mapped database 
        /// using the specified <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="transactionContext"></param>
        public void Save(DbTransaction transactionContext)
        {
            Save(false, transactionContext);
        }

        /// <summary>
        /// Pushes all CLR changes in this entity to the mapped database.
        /// If <paramref name="deep"/> is true, invokes
        /// the same Save(true) method on all of this entity's
        /// Entity members that are also DataModel types.
        /// </summary>
        /// <param name="deep"></param>
        public void Save(bool deep)
        {
            Save(deep, null);
        }

        /// <summary>
        /// Pushes all CLR changes in this entity to the mapped database.
        /// If <paramref name="deep"/> is true, invokes
        /// the same Save(true, transactionContext) method on all of this entity's
        /// Entity members that are also DataModel types.
        /// All of this is done within the specified <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="deep"></param>
        /// <param name="transactionContext"></param>
        public void Save(bool deep, DbTransaction transactionContext)
        {
            SynchronizeFields(SyncTo.FieldMappedData);
            if (IsNew || MarkDeleted || IsDirty)
            {
                if (DataProvider == null)
                {
                    throw new InvalidOperationException("The repository has not been assigned.");
                }
                if (deep)
                {
                    DataProvider.DeepSaveModel(this, transactionContext);
                }
                else
                {
                    DataProvider.SaveModel(this, transactionContext);
                }
            }
            SynchronizeFields(SyncTo.ClrMembers);
        }

        /// <summary>
        /// Gets or sets the value associated with this record based
        /// on the mapped field name rather than the CLR property
        /// name which is the default for default indexer this[].
        /// <example><code>int customerId = myDataModel.ColumnMappedValue["customer_id"];</code></example>
        /// </summary>
        protected internal ColumnMappedValueProperty ColumnMappedValue { get; private set; }

        /// <summary>
        /// Provides an indexer for <see cref="ColumnMappedValue"/>.
        /// </summary>
        protected internal class ColumnMappedValueProperty
        {
            private readonly DataModelMap Mapping;
            private readonly DataModel DataModel;
            internal ColumnMappedValueProperty(DataModel dataModel, DataModelMap mapping)
            {
                DataModel = dataModel;
                Mapping = mapping;
            }
            /// <summary>
            /// Gets or sets the value identified by the specified 
            /// <paramref name="key"/>, which corresponds to the 
            /// mapped database column name.
            /// </summary>
            /// <param name="key"></param>
            /// <returns></returns>
            public object this[string key]
            {
                get
                {
                    foreach (var field_kvp in Mapping.FieldMappings)
                    {
                        var field = field_kvp.Value;
                        if (field.ColumnName == null)
                        {
                            field.ColumnName = field_kvp.Key;
                        }
                        if (field.ColumnName.ToLower() == key.ToLower())
                        {
                            return DataModel[field_kvp.Key];
                        }
                    }
                    if (this.DataModel["field:" + key] != null)
                        return this.DataModel["field:" + key];
                    throw new ArgumentException("Field mapping not found");
                }
                set
                {
                    foreach (var field_kvp in Mapping.FieldMappings)
                    {
                        var field = field_kvp.Value;
                        if (field.ColumnName.ToLower() == key.ToLower())
                        {
                            this.DataModel[field_kvp.Key] = value;
                            return;
                        }
                    }
                    //throw new ArgumentException("Field mapping not found");
                    var attr = new DataModelFieldMappingAttribute(key);
                    attr.DataType = Gemli.Data.TypeConvertor.ToDbType(value.GetType());
                    Mapping.FieldMappings.Add("field:" + key, attr);
                    this.DataModel["field:" + key] = value;
                }
            }
        }

        /// <summary>
        /// Creates a new query.
        /// </summary>
        /// <returns></returns>
        public static DataModelQuery<DataModel> NewQuery()
        {
            return new DataModelQuery<DataModel>();
        }

        /// <summary>
        /// Gets or sets the database provider that is used to manage
        /// this entity's database storage.
        /// </summary>
        public Providers.DataProviderBase DataProvider { get; set; }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns></returns>
        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified object is equal to the current object.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            if (base.Equals(obj)) return true;
            if (!(obj is DataModel)) return false;
            var e = obj as DataModel;

            //this.SynchronizeFields(SyncTo.FieldMappedData);
            //e.SynchronizeFields(SyncTo.FieldMappedData);

            if (this.IsNew != e.IsNew) return false;
            if (this.MarkDeleted != e.MarkDeleted) return false;
            
            if (this.EntityMappings.TableMapping.Schema != e.EntityMappings.TableMapping.Schema ||
                this.EntityMappings.TableMapping.Table != e.EntityMappings.TableMapping.Table)
            {
                return false;
            }
            var checkedprops = new CaseInsensitiveStringList();
            foreach (var field_kvp in this.EntityMappings.FieldMappings)
            {
                string propname = null;
                foreach (var ef_kvp in e.EntityMappings.FieldMappings)
                {
                    if (field_kvp.Value.ColumnName.ToLower() == ef_kvp.Value.ColumnName.ToLower())
                    {
                        propname = field_kvp.Key;
                        checkedprops.Add(propname);
                        break;
                    }
                }
                if (propname == null) return false;
                if ((this.InnerData == null) != (e.InnerData == null)) return false;
                if (this.InnerData != null && e.InnerData != null &&
                    this.InnerData.ContainsKey(propname) != e.InnerData.ContainsKey(propname))
                    return false;
                if (this.InnerData != null)
                {
                    if (this.InnerData.ContainsKey(propname))
                    {
                        var thisProp = this[propname];
                        var eProp = e[propname];
                        if ((thisProp == null) != (eProp == null))
                            return false;
                        if (thisProp != null && !(thisProp.Equals(eProp)))
                        return false;
                    }
                }
            }
            foreach (var field_kvp in e.EntityMappings.FieldMappings)
            {
                if (checkedprops.Contains(field_kvp.Key)) continue;
                string propname = null;
                foreach (var ef_kvp in this.EntityMappings.FieldMappings)
                {
                    if (field_kvp.Value.ColumnName.ToLower() == ef_kvp.Value.ColumnName.ToLower())
                    {
                        propname = field_kvp.Key;
                        break;
                    }
                }
                if (propname == null) return false;
                if ((this.InnerData == null) != (e.InnerData == null)) return false;
                if (this.InnerData != null && e.InnerData != null)
                {
                    if (this.InnerData.ContainsKey(propname) != e.InnerData.ContainsKey(propname))
                        return false;
                    if (this.InnerData.ContainsKey(propname))
                    {
                        var thisProp = this[propname];
                        var eProp = e[propname];
                        if ((thisProp == null) != (eProp == null))
                            return false;
                        if (thisProp != null && !(thisProp.Equals(eProp)))
                            return false;
                    }
                }
            }
            return true;
        }

        #region INotifyPropertyChanged Members

        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add { DataChanged += value; }
            remove { DataChanged -= value; }
        }

        #endregion

        #region INotifyPropertyChanging Members

        event PropertyChangingEventHandler INotifyPropertyChanging.PropertyChanging
        {
            add { DataChanging += value; }
            remove { DataChanging -= value; }
        }

        #endregion
    }
}